Fix requirements.txt pins not matched under PEP 440 (#475) - #478
Mikola Lysenko (mikolalysenko) merged 5 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A hand-written pin such as `six==1.16` installs six 1.16.0, but the hosted requirements.txt rewrite compared versions as raw strings and skipped it, so `scan` exited 0 and the project stayed unpatched. The vendored requirements writer and the Hatch rewriter refused the same pins as "not pinned". Add a small PEP 440 equality helper (zero-padded release segments, leading zeros, case and pre/post/dev spellings) and use it for `==` pins in all three writers. `===` keeps plain string equality, as PEP 440 defines it. Fixes #475 Assisted-by: Claude Code:claude-opus-5-5
Mirrors the #475 repro end to end: `get <uuid> --mode hosted` over `requests==2.31`, `==2.31.0.0` and `Requests==02.31.0` must redirect the pin to the hosted wheel. Fails on main, passes with the fix. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Burn-down agent: ready for review at
Generated by Claude Code |
|
Reviewed Validation: 5 PEP 440-focused core tests, all 36 vendored-requirements unit tests, and the in-process hosted-get regression passed. Full workspace and real pip/uv matrices were not rerun. The separately documented lock-only discovery follow-up remains outside this change. |
|
…s-pep440-pin-match
Since #383, a hosted scan of a requirements.txt that is not in hash-checking mode pins the patched wheel with the url's #sha256= fragment rather than --hash, so the PEP 440 regression test now expects that form, matching the existing hosted pypi test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbFzTeYSTvB5iKjY6FRg7
|
Tanmay Singla (@Tanmay182003) Good catch, thanks. Fixed in c331d18. I merged Checked locally on the merged tree:
The existing Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c331d18. Configure here.
|
CI: This doesn't look like it comes from this PR:
I couldn't see which refusal code appeared, because the log only records the check name and the artifact isn't reachable from my sandbox. No fix for it exists yet. I'll re-run the failed job once the workflow finishes, since GitHub won't re-run it while the rest of the run is still going. If it fails again, I'll treat it as a real failure and dig in. Generated by Claude Code |
|
[agent] All checks green on Generated by Claude Code |
|
Second pass reviewed Validation at this exact head: |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #475
Summary
Hand-written requirements.txt pins like
six==1.16(for an installed six 1.16.0) are now matched the way pip matches them, under PEP 440 equality. Before this change,scan --mode hostedskipped such a pin, reported "no requirements.txt entry", exited 0, and left the project installing the unpatched release. This is a regression from v4.0.0, introduced in #239.Root cause
Every pin matcher compared the pinned version with the patch version as a raw string. pip compares them under PEP 440, which zero-pads release segments and ignores leading zeros, case and alternate pre/post/dev spellings. So
==1.16,==1.16.0.0and==01.16.0all select exactly 1.16.0. The same raw comparison was in three writers:patch/redirect/requirements.rsskipped the pin withredirect_requirements_entry_not_foundand exited 0;vendor/pypi_requirements.rsrefused with a falsepypi_requirement_not_pinned;utils/hatch.rs(used by hosted and vendored Hatch) refused hand-writtenpyproject.tomlpins likeurllib3==1.26.18.0with "requires an exact ==1.26.18 declaration". Same cause at the same boundary, so it is fixed here too.Fix
utils/pep440.rs: a small parser for thepackagingversion grammar, plus PEP 440 equality (epoch, zero-trimmed release, normalized pre/post/dev, case-folded local segments). Digit runs are compared as leading-zero-trimmed strings, so long numbers can't overflow. Invalid versions are never equal to anything, so callers fail closed.==pins.===(arbitrary equality) keeps plain string comparison, as PEP 440 defines it, and wildcards (==1.*) are still ranges.Notes / follow-ups
name==<patch version>. A redirectedsix==1.16therefore rolls back tosix==1.16.0, which installs the same release. This matches how rollback already normalizessix == 1.16.0today. Vendored revert is ledger-based and stays byte-exact.utils/requirements.rs::exact_pinstill carries the spelled version into the purl (pkg:pypi/six@1.16). Matching that to the1.16.0release needs PyPI's canonical release spelling, not just local normalization. The issue lists this as a "may" and its repro uses an installed venv, so it's left as a follow-up rather than guessed at here.Test evidence
Regression tests, each shown failing on main and passing with the fix:
==X.Y,==X.Y.Z.0,==0X.Y.Z, spaced + markerpatch::redirect::requirements::tests::pep440_equivalent_pins_are_rewrittenredirect_requirements_entry_not_foundget <uuid> --mode hostedoverrequests==2.31,==2.31.0.0,Requests==02.31.0)in_process_get_hosted_ecosystems::pypi_requirements_hosted_rewrites_pep440_equivalent_pinpypi_requirement_not_pinnedvendor::pypi_requirements::tests::find_pin_classifies_every_shape(new PEP 440 cases;===/ wildcard still Range)utils::hatch::tests::pep440_equivalent_pins_are_exact_declarationsutils::pep440::tests::*(equal spellings, non-equal releases, invalid input,==vs===/wildcards)Local runs (Linux):
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test --workspace --all-features --no-fail-fast: everything passes except 12 permission-based write-failure tests, which fail because this sandbox runs as uid 0 (root ignores read-only bits). They fail identically onmain, and all 12 pass when re-run as uid 65534.cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --ignored(real uv + pip + PyPI): 7/7 pass.cargo fmt: the changed code is rustfmt-formatted. CI has no fmt step andmainitself isn't fmt-clean (rustfmt 1.8 rewrites ~125 unrelated files), so unrelated reflows were left out.npm/,pypi/andgem/only dispatch to the binary.🤖 Generated with Claude Code
Note
Medium Risk
Changes pin-matching on the patch redirect path for PyPI; incorrect PEP 440 logic could mis-redirect or skip pins, but invalid versions fail closed and
===/wildcards are unchanged.Overview
Fixes #475: PyPI pin matching now treats
==the same way pip/uv/Hatch do under PEP 440, instead of comparing version strings literally.Adds
utils/pep440with packaging-style parsing and equality (versions_equal,is_exact_pin_of). Hostedrequirements.txtredirect, vendoredpypi_requirementspin discovery, and Hatchpyproject.tomlrewrites all use it for==pins.===stays plain string equality; wildcards and ranges are unchanged.Pins like
requests==2.31,==2.31.0.0, or==02.31.0now redirect to the patched wheel when the grant is2.31.0, instead of skipping with “no entry” / false “not pinned” and leaving the project unpatched.Regression coverage: unit tests for the helper and each rewriter, plus an in-process
get --mode hostedtest over equivalentrequirements.txtpins.Reviewed by Cursor Bugbot for commit bcc5335. Configure here.
Generated by Claude Code